package net.digitalprimates.flex2.uint.tests
{
  import flash.events.Event;
  import flash.events.EventDispatcher;
  import flash.events.IEventDispatcher;
  import flash.events.MouseEvent;
  import flash.events.TimerEvent;
  import flash.utils.Timer;
  
  import mx.binding.utils.BindingUtils;
  import mx.collections.ICollectionView;
  import mx.collections.IList;
  import mx.containers.VBox;
  import mx.controls.Alert;
  import mx.controls.ComboBox;
  import mx.controls.DataGrid;
  import mx.controls.HRule;
  import mx.controls.Text;
  import mx.controls.listClasses.IListItemRenderer;
  import mx.core.Application;
  import mx.core.IChildList;
  import mx.core.ScrollPolicy;
  import mx.core.UIComponent;
  import mx.events.FlexEvent;
  import mx.managers.ISystemManager;
  import mx.managers.PopUpManager;
  import mx.rpc.http.HTTPService;
  import mx.utils.ObjectUtil;
  
  import net.digitalprimates.flex2.uint.monitor.TestMethodResult;
  import net.digitalprimates.flex2.uint.sequence.*;
  import net.digitalprimates.flex2.uint.ui.TestResultDisplay;
  import net.digitalprimates.flex2.uint.utils.LoggerUtils;
  
  
  /**
   * TestCase which can load a UI component loaded from a module. 
   */
  public class ComponentTestCase extends TestCase
  {
    private var loader:Function;
    
    /** Reference the current UIComponent under test. This gets reset with *every* test. */
    private var _uiComponent:UIComponent;
    
    private var moduleTestContainer:Object;
    private var testResults : TestResultDisplay;
    private var allTestResultsContainer:VBox;
    
    /**
     * Creates a new ComponentTestCase.
     * 
     * @param loader a function which will create a new UIComponent for every test
     */
    public function ComponentTestCase(loader : Function)
    {
      this.loader = loader;
      this.moduleTestContainer = new Object();
      
      allTestResultsContainer = new VBox();
      allTestResultsContainer.percentWidth = 100;
      allTestResultsContainer.height = 500;
      allTestResultsContainer.verticalScrollPolicy = ScrollPolicy.AUTO;
      
      Application.application.addChild(allTestResultsContainer);
      
      testResults = (Application.application as Application).getChildByName("testResultDisplay") as TestResultDisplay;
      BindingUtils.bindSetter(testItemSelected, testResults.testTree, "selectedItem");
    }
    
    // TestRunner 
    
    /**
     * Highlights the selected test items UI container.
     */
    private function testItemSelected(selectedName:String) : void 
    {
      var testResult : TestMethodResult = testResults.testTree.selectedItem as TestMethodResult
      
      // Checking that we're on an actual test result, and not one of the grouping nodes.
      if (testResult)
      {
        // Reset styles on all test containers
        for (var propName : String in moduleTestContainer)
        {
          if (moduleTestContainer[propName].container is UIComponent) 
          {
            var container : UIComponent = moduleTestContainer[propName].container as UIComponent;  
            container.setStyle("borderStyle", "");
            container.setStyle("borderColor", "");
            container.setStyle("borderThickness", "");  
          }
        }
        
        // Apply style to selected test
        var testName : String = testResult.xmlResults.@name;
        var testContainer : UIComponent = (moduleTestContainer[testName].container as UIComponent);
        if (testContainer) 
        {
          testContainer.setStyle("borderStyle", "solid");
          testContainer.setStyle("borderColor", testResult.isError || !testResult.status ? "red" : "green");
          testContainer.setStyle("borderThickness", "4");
          
          // The last test run is at the top of the container
          // TODO This is a really crude calculation of the testContainer's position in allTestResultsContainer
          //      Also, it would be nice to have a scroll effect when you select a test result.
          var testRun : int = moduleTestContainer[testName].testRun as int;
          var indexFromTop : int = (currentTestIndex - testRun);
          allTestResultsContainer.verticalScrollPosition = testContainer.measuredHeight * indexFromTop;
        } 
      }
    }
    
    override protected function setUp():void {
      trace("");
      trace("-------Setup");
      
      _uiComponent = loader();
      
      var testContainer : VBox = new VBox();
      testContainer.percentWidth = 100;
      
      var separator : HRule = new HRule();
      separator.percentWidth = 100;
      testContainer.addChild(separator);
      
      var testLabel : Text = new Text();
      testLabel.setStyle("fontSize", "14");
      testLabel.setStyle("fontWeight", "bold");
      testLabel.text = currentTestName;
      
      testContainer.addChild(testLabel);
      testContainer.addChild(_uiComponent);
      
      moduleTestContainer[currentTestName] = {container:testContainer, testRun:currentTestIndex};
      
      testContainer.addEventListener(FlexEvent.CREATION_COMPLETE, asyncHandler(creationComplete, 5000));
      
      this.sequence = new SequenceRunner(this);
      
      // Add to the beginning of the container so you can watch the _uiComponent while it is being tested
      allTestResultsContainer.addChildAt(testContainer, 0);
    }
    
    /**
     * Called when the _uiComponent has finished inializing for this test method.
     */
    private function creationComplete(event : Event,  passThroughData : Object):void {
      uiComponentReady();
      restoreState();  
    }
    
    /**
     * Called before every test method with a valid SequenceRunner and valid uiComponent to enable restoration of the server.
     * 
     * Subclasses should override this method if they need to restore the state of the server.  Restoration of the state of the
     * client is done automatically as every test method uses a completely new instance of the component under test.
     */
    protected function restoreState() : void 
    {
    } 
    
    /**
     * Called when the UI Component under test is ready to be accessed.
     * 
     * It is assumed subclasses will override this and cast the component to an instance variable of the correct type 
     * to make code completion work.
     */
    protected function uiComponentReady() : void {
    }
    
    protected function childRemoved(event : Event,  passThroughData : Object):void {
      this._uiComponent = null;
    }
    
    /**
     * Unloads the UI component under test.
     * 
     * You don't want to modify the state of your _uiComponent in the teardown as its appearence in the test runner should
     * reflect its final state.  This convention greatly assists test writers to see the finished state of the component
     * after the test has ran.
     */
    override protected function tearDown():void {
      this.sequence = null;
      
      // Close all popups so you can see the results.
      var systemManager : ISystemManager = Application.application.systemManager;
      var children : IChildList = systemManager.rawChildren as IChildList;
      for (var i : int = 0; i < children.numChildren; i++) {
        var child : UIComponent = children.getChildAt(i) as UIComponent;
        if (child is Alert && (child as Alert).visible) 
        {
          PopUpManager.removePopUp(child);
        }
      }
    }
    
    public function get uiComponent() : Object 
    {
      return this._uiComponent;
    }
    
    // --------- Sequence Helper Methods
    
    protected var sequence : SequenceRunner;
    
    /**
     * Generic timeout handler for all SequenceWaiters.
     */ 
    private function timeoutHandler(e : SequenceRunner) : void 
    {
      fail("Timed out waiting for event: " + e.getPendingStep().eventName + " on " + e.getPendingStep().target);
    }
    
    /**
     * Simulate a user clicking on the target.
     * 
     * @param target any UIComponent capable of dispatching mouse click events or a function which returns an event dispatcher
     */
    public function clickOn( target:Object ) : void 
    {
      assertState(function():void {
        assertEnabled(target);
        assertVisible(target);
      });
        
      sequence.addStep(new SequenceEventDispatcher( target, new MouseEvent('click', true, false) ));  
    }
    
    /**
     * Simulate typing the given value into the target.
     * 
     * @param value the string value to enter into the UIComponent
     * @param target any UIComponent (or Function which returns such) with a text property
     */  
    public function typeInto( value:String, target:Object ) : void 
    {
      var props : Object = {text:value};
        
      trace("Setting '" + value + "' into '" + LoggerUtils.friendlyName(target) + "'");
      assertState(function():void {
        assertEnabled(target);
        assertVisible(target);
      });
      
      sequence.addStep( new SequenceSetter( target, props ) );
      
      // Blank values will not trigger validation, so we need to fake it
      if (value == "") 
      {
        triggerValidate( target );
      }  
      
      sequence.addStep( new SequenceWaiter( target, FlexEvent.VALUE_COMMIT, 1000, timeoutHandler ));
    }
    
    /**
     * Simulate selecting an item in a ComboBox with the specified value.
     * 
     * @param value value to select
     * @param target ComboBox to select from
     */
    public function select( value:Object, target:ComboBox ) : void 
    {
      trace("Selecting '" + value + "' for '" + LoggerUtils.friendlyName(target) + "'");
      assertState(function():void {
        assertEnabled(target);
        assertVisible(target);
      });
      
      sequence.addStep( new SequenceSetter( target, {selectedItem:value} ) );
      sequence.addStep( new SequenceWaiter( target, FlexEvent.VALUE_COMMIT, 500, timeoutHandler ));
    }

    /**
     * Simulate selecting an item in a ComboBox with the specified index.
     * 
     * @param value index to select.
     * @param target ComboBox to select from
     */
    public function selectByIndex( value:Object, target:ComboBox ) : void 
    {
      trace("Selecting '" + value + "' for '" + LoggerUtils.friendlyName(target) + "'");
      assertState(function():void {
        assertEnabled(target);
        assertVisible(target);
      });
      
      sequence.addStep( new SequenceSetter( target, {selectedIndex:value} ) );
      sequence.addStep( new SequenceWaiter( target, FlexEvent.VALUE_COMMIT, 500, timeoutHandler ));
    }

    
    /**
     * Wait for the specified event to occur.
     * 
     * @param target the source of the event
     * @param eventName the name of the event
     * @param waitTimeInMilliseconds the amount of time in milliseconds to wait for specified event 
     */
    public function waitFor( target:IEventDispatcher, eventName : String, waitTimeInMilliseconds : int = 2000, 
                             customTimeoutHandler:Function = null) : void
    {
      var identifier : String = target + "";
      if (target is UIComponent)
      {
        identifier = (target as UIComponent).id;
      } 
      else if (target is HTTPService)
      {
        identifier = (target as HTTPService).url;
      }
      trace("Waiting for '" + eventName + "' on '" + identifier + "'");
      
      sequence.addStep(new SequenceWaiter(target, eventName, waitTimeInMilliseconds, (customTimeoutHandler != null) ? customTimeoutHandler : timeoutHandler ));
    }
    
    /**
     * Wait until the specified function evaluates to true.  The condition will be checked every 50ms until either the condition
     * evaluates to true or the timeout is reached.  If the timeout is reached before the condition is true, the test will fail.
     */
    public function waitUntil( condition:Function, failureMessage:String = "Timed out waiting for condition.", timeout:int = 5000) : void
    {
      // Check condition every 50ms and timeout after specified time
      var sleeper : SequenceSleep = new SequenceSleep(50, timeout/50)
      sequence.addStep(sleeper);
      
      sleeper.target.addEventListener(TimerEvent.TIMER, function(e:TimerEvent) : void {
        if (condition() == true) {
          e.target.dispatchEvent(new Event("sequenceConditionMet"));
          e.target.stop();
        }        
      });
      sleeper.target.addEventListener(TimerEvent.TIMER_COMPLETE, function(e:TimerEvent) : void {
        if (condition() == true) {
          e.target.dispatchEvent(new Event("sequenceConditionMet"));
        } 
        e.target.stop();
      });
      
      // Fail if time out while waiting for condition
      waitFor(sleeper.target, "sequenceConditionMet", timeout, function():void {
        fail(failureMessage);
      });
    }
    
    /**
     * Wait for a specified time until a popup occurs with the given message.
     * 
     * If alert never occurs within the given timeout, the test will fail.
     */
    public function waitForPopup( expectedMessage:String, timeout : int = 5000) : void
    {
      waitUntil(function():Boolean {
        return alertMessageExistsWithMessage(expectedMessage);
      }, "Timed out waiting for popup with message: '" + expectedMessage + "'");
    }
    
    /**
     * Invoke the specified function in the sequence.
     * 
     * @param object object where method lives
     * @param methodName name of method
     * @param params arguments to pass to method
     */
    public function invoke( object:Object, methodName:String, params:Array = null) : void 
    {
      trace("Invoke '" + methodName + "' on " + object);
      sequence.addStep(new SequenceAction(object, methodName, params));  
    }
    
    /**
     * Asserts the state at the end of the test.  This should always be the LAST method call in EVERY test.
     * 
     * TODO: Change to assertEndState to keep parallel to assertState
     * 
     * @param assertion the function containing the assertions
     * @param passThru TBD
     */
    public function assertFinished( assertion:Function, passThru:Object = null ) : void 
    {
      trace("Running Sequence....");
      trace("");
      
      sequence.addAssertHandler(assertion, passThru);
      sequence.run();
    }
    
    /**
     * Plays the recorded sequence.
     */
    public function play() : void
    {
      sequence.run();
    }
    
    /**
     * Assert state in the middle of a sequence.
     * 
     * @param assertion Function which contains asssertions
     */
    public function assertState( assertion:Function) : void 
    {
      trace("Asserting the current state of the module.");
      sequence.addStep(new SequenceAssert(assertion));
    }
    
    /**
     * Trigger validation on the specified component.
     * 
     * This is required to test validation of empty text fields since validation usually only occurs after the user has 
     * entered some value.
     */
    public function triggerValidate( target:Object ) : void 
    {
      trace("Triggering validate for '" + LoggerUtils.friendlyName(target) + "'");
      sequence.addStep( new SequenceEventDispatcher( target, new FlexEvent(FlexEvent.VALUE_COMMIT) ));
    }
    
    /**
     * Simulate clicking on a cell in a datagrid.
     * 
     * @param rowNum row number of the cell location (0-based, does not include header row)
     * @param columnNumb column number of cell location (0-based)
     * @param target datagrid
     */
    public function selectCell( rowNum:int, columnNum:int, target:DataGrid ) : void 
    {
      trace("Selecting row [" + rowNum + "], column [" + columnNum + "] on " + LoggerUtils.friendlyName(target))
      
      sequence.addStep( new SequenceSetter( target, {editedItemPosition: {rowIndex: rowNum, columnIndex: columnNum}} ));
      sequence.addStep( new SequenceWaiter( target, FlexEvent.VALUE_COMMIT, 1000, timeoutHandler ));
    }

    /**
     * Removes focus from currently selected cell, if there is one.
     * 
     * This is required to complete the editing of a cell.
     */
    public function unselectCell ( target: DataGrid ) : void
    {
      sequence.addStep( new SequenceSetter( target, {editedItemPosition: null} ));
      sequence.addStep( new SequenceSetter( target, {selectedIndex: null} ));
    }
    
    /**
     * Selects the specified row in a dataGrid.
     * 
     * @param rowNum row number (0-based index not including header)
     * @param target datagrid to select on
     */
    public function selectRow( rowNum:int, target:DataGrid ) : void 
    {
      trace("Selecting row'" + rowNum + "' on '" + LoggerUtils.friendlyName(target) + "'");
      assertState(function():void {
        assertEnabled(target);
        assertVisible(target);
      });
      
      sequence.addStep( new SequenceSetter( target, {selectedIndex: rowNum} ) );
      sequence.addStep( new SequenceWaiter( target, FlexEvent.VALUE_COMMIT, 500, timeoutHandler ));
    }
    
    /**
     * Wait a specified number of milliseconds before continuing.
     * 
     * This is a very crude way to introduce waiting unto your system.  The preferred method is to use the #waitUntil method
     * which will only wait as long as needed until the desired condition is met, making your tests run faster and more reliably.
     */
    public function delay(millseconds:int = 1000) : void
    {
      var sleeper : SequenceSleep = new SequenceSleep(millseconds)
      sequence.addStep(sleeper);
      waitFor(sleeper.target, TimerEvent.TIMER, millseconds + 2000);
    }
    
    // --- Selectors
    
    /**
     * Delayed selector for the current cell in a datagrid.
     */
    public function currentCell(dataGrid:DataGrid) : Function
    {
      return function() : IListItemRenderer {
        if (!dataGrid.itemEditorInstance)
        {
          fail("No cell selected in grid [" + dataGrid.id + "]");
        }
        return dataGrid.itemEditorInstance;
      };
    }
    
    public function cell(row:int, column:int, dataGrid:DataGrid) : Function
    {
      selectCell(row, column, dataGrid);
      waitUntil(function():Boolean {
        return currentCell(dataGrid) != null;
      });
      
      return currentCell(dataGrid);
    }
    
    /**
     * Delayed selector for specified element in specified row.
     */
    public function elementInRow(elementId:String, rowNum:int, dataGrid:DataGrid) : Function
    {
      selectRow(rowNum, dataGrid);
      
      return function() : EventDispatcher {
        var row : Object = dataGrid.indexToItemRenderer(rowNum);
        if (row) {
          return row[elementId];
        } else {
          fail("No renderer available for row number [" + rowNum + "] in " + dataGrid.id);
          return null;
        }
      };
    }
    
    // --------- Custom Asserts.  These asserts either take the direct UIComponent or a Function reference to one
    
    /**
     * Processes the target parameter as either a UIComponent or as a reference to a UIComponent through a Function.
     */
    private function processComponentReference( target:Object) : UIComponent 
    {
      if (target is Function)
      {
        return (target as Function)();
      }
      else if (target is UIComponent)
      {
        return target as UIComponent;
      }
      else
      {
        fail("Invalid type: " + target);
        return null;
      }
    }
    
    public function assertVisible( target:Object) : void 
    {
      var component : UIComponent = processComponentReference(target);
      assertTrue("Expecting component '" + component.id + "' to be visible", component.visible);
    }
    
    public function assertHidden( target:Object) : void 
    {
      var component : UIComponent = processComponentReference(target);
      assertFalse("Expecting component '" + component.id + "' to be hidden", component.visible);
    }
    
    public function assertEnabled( target:Object) : void 
    {
      var component : UIComponent = processComponentReference(target);
      assertTrue("Expecting component '" + component.id + "' to be enabled", component.enabled);
    }
    
    public function assertDisabled( target:Object) : void 
    {
      var component : UIComponent = processComponentReference(target);
      assertFalse("Expecting component '" + component.id + "' to be disabled", component.enabled);
    }
    
    public function assertInvalid( target:Object) : void 
    {
      var component : UIComponent = processComponentReference(target);
      assertTrue("Expecting property '" + component.id + "' to be invalid.", component.errorString.length > 0);
    }
    
    public function assertValid( target:Object) : void 
    {
      var component : UIComponent = processComponentReference(target);
      assertTrue("Expecting an empty error string on '" + component.id + "', got '" + component.errorString + "' instead.", 
        component.errorString.length == 0);
    }

    /**
     * Assert that an Alert message exists with the specified string.
     */
    public function assertAlertMessage(expectedMessage:String) : void 
    {
      assertTrue("Did not find an alert with the message '" + expectedMessage + "'", alertMessageExistsWithMessage(expectedMessage));
    }
    
    protected function alertMessageExistsWithMessage(expectedMessage:String):Boolean 
    {
     var systemManager : ISystemManager = Application.application.systemManager;
      var children : IChildList = systemManager.rawChildren as IChildList;
      var foundAlertMessage : Boolean = false;
      for (var i : int = 0; i < children.numChildren; i++) {
        var child : UIComponent = children.getChildAt(i) as UIComponent;
        if (child is Alert && (child as Alert).visible) 
        {
            if(expectedMessage == (child as Alert).text)
            {
              foundAlertMessage = true;
              break;
            }
        }
      } 
      return foundAlertMessage;
    }
  }
}